import bpy
from uuid import uuid4
from typing import List, Union
from collections import defaultdict
from bpy.props import StringProperty
from ...addon.naming import FluidLabNaming
from ...libs.functions.object import name_with_zfill
from ...libs.functions.basics import set_active_object
from ...libs.functions.geometry_nodes import get_node_index_or_identifier_by
from ...libs.functions.get_common_vars import get_common_vars
from bpy.types import Context, Operator, Menu, Object, Collection
from ...properties.lists.updaters.props_update_particles import ui_update
from ...properties.lists.props_list_fluid_groups import FluidGroupsListItem
from ...properties.lists.props_list_fluid_emitters import FluidEmittersListItem
from ...libs.functions.collections import set_active_collection, create_new_collection
from random import randint


###################################
""" Dropdowns lists Utilities   """
###################################


class FLUIDLAB_OT_list_item_move(Operator):
    bl_idname = "fluidlab.list_item_move"
    bl_label = "Move Item"
    bl_description = "Move Item in List"
    bl_options = {'REGISTER', 'UNDO'}

    """ Base Class for Move items in lists """

    direction: StringProperty(default="")

    def reorder_list(self, target_list, direction:str) -> None:

        """ Es necesario que el target_list teng dentro un .list y un .list_index """

        if not target_list:
            print("[reorder list]: Invalid input list!, exit")
            return

        if direction not in ('UP', 'DOWN'):
            print("[reorder list]: Invalid direction!, exit")
            return

        index = target_list.list_index

        # move item:
        neighbor = index + (-1 if direction == 'UP' else 1)
        target_list.list.move(neighbor, index)

        # re set active item in the list:
        list_length = len(target_list.list) - 1
        target_list.list_index = max(0, min(neighbor, list_length))


#--------------------------------------
# Fluid Group and  Emitters Orders ops:
#--------------------------------------
class FLUIDLAB_OT_group_list_item_move(FLUIDLAB_OT_list_item_move):
    bl_idname = "fluidlab.group_list_item_move"
    bl_label = "Move Item"
    bl_description = "Move Item in List"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        self.reorder_list(fluid_groups, self.direction)
        return {'FINISHED'}


class FLUIDLAB_OT_emitters_list_item_move(FLUIDLAB_OT_list_item_move):
    bl_idname = "fluidlab.emitters_list_item_move"
    bl_label = "Move Item"
    bl_description = "Move Item in List"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        fluid_groups = get_common_vars(context, get_fluid_groups=True)

        if not fluid_groups.is_void:
    
            active_group = fluid_groups.active
            emitters_list = active_group.emitters
    
            if not emitters_list.is_void:
                self.reorder_list(emitters_list, self.direction)
    
        return {'FINISHED'}

#-------------------------
# Forces lists Orders ops:
#-------------------------
class FLUIDLAB_OT_force_groups_list_item_move(FLUIDLAB_OT_list_item_move):
    bl_idname = "fluidlab.force_groups_list_item_move"
    bl_label = "Move Item"
    bl_description = "Move Item in List"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        self.reorder_list(fluid_forces_groups, self.direction)
        return {'FINISHED'}

class FLUIDLAB_OT_force_groups_forces_list_item_move(FLUIDLAB_OT_list_item_move):
    bl_idname = "fluidlab.force_groups_forces_list_item_move"
    bl_label = "Move Item"
    bl_description = "Move Item in List"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        ff_group_active = fluid_forces_groups.active
        
        if ff_group_active:
            forces_list = ff_group_active.forces
            self.reorder_list(forces_list, self.direction)
        
        return {'FINISHED'}



###################################
""" Dropdowns lists Operators  """
###################################


class FLUIDLAB_OT_list_duplication(Operator):
    bl_idname = "fluidlab.lists_duplication"
    bl_label = "List Duplication"
    bl_description = "List Duplication"
    bl_options = {'REGISTER', 'UNDO'}


    """ Base Class for common methods of duplication """


    def make_unique_materials(self, context:Context, new_object:Object) -> None:

        for i, mat_slot in enumerate(new_object.material_slots):
            mat = mat_slot.material

            # New mat name:
            base_name = "Material"
            place = bpy.data.materials
            desired_name = base_name
            new_mat_name = name_with_zfill(context, desired_name, place)
            mat_copy = mat.copy()
            mat_copy.name = new_mat_name

            new_object.data.materials[i] = mat_copy


    def duplicate_ob(self, context:Context, in_coll:Collection, original_object:Object) -> Object:
        
        # Duplica el objeto
        new_object = original_object.copy()
        new_object.data = original_object.data.copy()
        if new_object.animation_data:
            new_object.animation_data.action = original_object.animation_data.action.copy()

        # New name:
        place = context.view_layer.objects
        desired_name = original_object.name
        new_name = name_with_zfill(context, desired_name, place)
        new_object.name = new_name
        new_object.data.name = new_name

        # Import new ob to collection:
        if new_object.name not in in_coll.objects:
            in_coll.objects.link(new_object)

        # Selecciona el nuevo objeto
        set_active_object(context, new_object)
        new_object.select_set(True)

        # Haciendo unicos sus materiales:
        self.make_unique_materials(context, new_object)

        return new_object
    

    def setup_particles(self, context:Context, original_object:Object, new_emtr_ob:Object) -> None:

        # Ya trae las particulas del objeto original, pero ahora vamos a hacer que sean un duplicado:
        new_particle_system = new_emtr_ob.particle_systems[-1]

        # Copia las configuraciones del sistema de partículas original al nuevo
        original_particle_system = original_object.particle_systems[-1]
        new_particle_system.settings = original_particle_system.settings.copy()

        # Hacemos cada action de las particulas único:
        ps_ad = new_particle_system.settings.animation_data
        if ps_ad:
            new_action = ps_ad.action.copy()
            ps_ad.action = new_action

        # Seteamos el nuevo nombre de la cache:
        new_particle_system.point_cache.name = new_emtr_ob.name

    
    def duplicate_vertex_ob(self, context:Context, active_group:FluidGroupsListItem, emitter_item:FluidEmittersListItem, new_emitter_ob:Object, is_dead:bool = False) -> Object:
        
        """ 
            Duplico el vertex ob alive y/o el vertex ob dead 
            cada uno con sus configuraciones correspondientes.
        """

        vertex_coll = active_group.vertex_coll
        vertex_ob = emitter_item.vertex_ob_dead if is_dead else emitter_item.vertex_ob
        
        if vertex_ob:

            new_vrtx_ob = self.duplicate_ob(context, vertex_coll, vertex_ob)
            part_inst_mod_name = FluidLabNaming.PARTICLE_INSTANCE_DEAD_MOD if is_dead else FluidLabNaming.PARTICLE_INSTANCE_ALIVE_MOD
            part_inst_mod = new_vrtx_ob.modifiers.get(part_inst_mod_name)
            
            if part_inst_mod:
                part_inst_mod.object = new_emitter_ob
                part_inst_mod.show_alive = not is_dead
                part_inst_mod.show_dead = is_dead
                part_inst_mod.show_unborn = False
                new_vrtx_ob[FluidLabNaming.PARTICLE_INSTANCER_OB] = True
                new_vrtx_ob.hide_set(True)
                new_vrtx_ob.hide_render = True

            return new_vrtx_ob
    
    
    def re_set_mesh_mod_ob(self, new_vrtx_ob, new_vrtx_ob_dead):
        # Hay que volver a setear el mesh modifier del vertex ob alive (particle instancer):
        mesh_mod = new_vrtx_ob.modifiers.get(FluidLabNaming.GN_MESH_MOD)
        if mesh_mod:
            g_input = next((g_input for g_input in mesh_mod.node_group.nodes if g_input.type == 'GROUP_INPUT'), None)
            if g_input:
                shocket = get_node_index_or_identifier_by("identifier", "name", "outputs", g_input, "Object", debug=False)
                if shocket in mesh_mod:
                    mesh_mod[shocket] = new_vrtx_ob_dead


    def duplicate_coll(self, context:Context, fluid_group_coll:Collection, group_coll:Collection) -> List[Union[str, Collection]]:

        set_active_collection(context, fluid_group_coll)

        # New name:
        place = bpy.data.collections
        desired_name = group_coll.name
        new_name = name_with_zfill(context, desired_name, place)
        
        new_coll = create_new_collection(context, new_name, False)
        new_coll[FluidLabNaming.FluidLab] = True
        new_coll[FluidLabNaming._GROUPS_COLL] = True

        return [new_name, new_coll]


class FLUIDLAB_OT_duplicate_group(FLUIDLAB_OT_list_duplication):
    bl_idname = "fluidlab.duplicate_group"
    bl_label = "Duplicate Group"
    bl_description = "Duplicate Group Iten"
    bl_options = {'REGISTER', 'UNDO'}


    def execute(self, context):
        
        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group_org = fluid_groups.active
        group_coll = active_group_org.group_coll

        fluid_group_coll = bpy.data.collections.get(FluidLabNaming.FLUID_GROUPS_COLL)
        if not fluid_group_coll:
            return {'CANCELLED'}

        new_name, new_coll = self.duplicate_coll(context, fluid_group_coll, group_coll)

        emitters_list = active_group_org.emitters
        all_emitters = emitters_list.get_all_emitters
        
        # Guardamos el vertex_coll para luego setearselo al nuevo grupo:
        vertex_coll = active_group_org.vertex_coll

        if emitters_list.length > 0:
            # Guardamos el prev_size para luego setearselo al nuevo grupo:
            # emitter_ob = emitters_list.get_current_emitter
            emitter_item = emitters_list.active
            prev_size = emitter_item.emission.prev_size
        else:
            prev_size = -999999999


        emitters_dict = {}
        emitters_switchers = defaultdict(list)

        for emitter_ob in all_emitters:

            # Duplicate Emitter Object:
            new_emtr_ob = self.duplicate_ob(context, new_coll, emitter_ob)

            # Configuramos las particulas y su cache name:
            self.setup_particles(context, emitter_ob, new_emtr_ob)
            
            # Duplicate Single Vertex Object:  
            new_vrtx_ob = self.duplicate_vertex_ob(context, active_group_org, emitter_item, new_emtr_ob, is_dead=False)

            # Duplicate Single Vertex Dead Object:  
            new_vrtx_ob_dead = self.duplicate_vertex_ob(context, active_group_org, emitter_item, new_emtr_ob, is_dead=True)

            name_id = str(uuid4())[:6]

            # Lo marcamos como Alive:
            new_vrtx_ob[FluidLabNaming.ALIVE] = True
            new_vrtx_ob.name = FluidLabNaming.VERTEX_PREFIX + name_id + "_Alive"

            # Lo marcamos como Dead:
            new_vrtx_ob_dead[FluidLabNaming.DEAD] = True
            new_vrtx_ob_dead.name = FluidLabNaming.VERTEX_PREFIX + name_id + "_Dead"
            
            # Hay que volver a setear el mesh modifier pero del particle instancer vertex:
            self.re_set_mesh_mod_ob(new_vrtx_ob, new_vrtx_ob_dead)
            
            # Guardamos los datos para luego incluirlos en su listado:
            vertex_id_name = str(uuid4())[:6]
            emitters_dict[vertex_id_name] = (new_emtr_ob.name, new_emtr_ob, new_vrtx_ob, new_vrtx_ob_dead)
        
            # Guardo los estados de los switchers para Global/Local:
            for cp_name in emitter_item.switchers.__annotations__.keys():
                emitters_switchers[vertex_id_name].append((cp_name, getattr(emitter_item.switchers, cp_name)))

        # Agregamos el nuevo grupo en el listado de grupos:
        group_id_name = str(uuid4())[:6]
        new_active_group = fluid_groups.add_item(group_id_name, new_name, new_coll, active_group_org.emitter_type, vertex_coll, prev_size)
        
        # Agregamos los emitters en el nuevo grupo:
        emitters_list = new_active_group.emitters
        
        for vertex_id_name, values in emitters_dict.items():
            new_emtr_ob_name, new_emtr_ob, new_vrtx_ob, new_vrtx_ob_dead = values
            emitters_list.add_item(vertex_id_name, new_emtr_ob_name, new_coll, new_emtr_ob, new_vrtx_ob, new_vrtx_ob_dead)

        return {'FINISHED'}


class FLUIDLAB_OT_duplicate_emitter(FLUIDLAB_OT_list_duplication):
    bl_idname = "fluidlab.duplicate_emitter"
    bl_label = "Duplicate Emitter"
    bl_description = "Duplicate Emitter Iten"
    bl_options = {'REGISTER', 'UNDO'}


    def execute(self, context):

        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active

        if not active_group:
            print("No active group!")
            return {'CANCELLED'}
        
        emitters_list = active_group.emitters
        active_emitter = emitters_list.active

        if not active_emitter:
            print("No active emitter!")
            return {'CANCELLED'}

        emitter_ob = emitters_list.get_current_emitter

        group_coll = active_group.group_coll

        # Selecciona el objeto con el sistema de partículas original
        original_object = emitter_ob

        new_emtr_ob = self.duplicate_ob(context, group_coll, emitter_ob)

        # Configuramos las particulas y su cache name:
        self.setup_particles(context, original_object, new_emtr_ob)

        # Obtengo la infomación para duplicar su vertex ob:
        new_vrtx_ob = self.duplicate_vertex_ob(context, active_group, active_emitter, new_emtr_ob, is_dead=False)

        # Duplicate Single Vertex Dead Object:  
        new_vrtx_ob_dead = self.duplicate_vertex_ob(context, active_group, active_emitter, new_emtr_ob, is_dead=True)

        name_id = str(uuid4())[:6]

        # Lo marcamos como Alive:
        new_vrtx_ob[FluidLabNaming.ALIVE] = True
        new_vrtx_ob.name = FluidLabNaming.VERTEX_PREFIX + name_id + "_Alive"

        # Lo marcamos como Dead:
        new_vrtx_ob_dead[FluidLabNaming.DEAD] = True
        new_vrtx_ob_dead.name = FluidLabNaming.VERTEX_PREFIX + name_id + "_Dead"

        # Hay que volver a setear el mesh modifier pero del particle instancer vertex:
        self.re_set_mesh_mod_ob(new_vrtx_ob, new_vrtx_ob_dead)

        id_name = str(uuid4())[:6]

        # Guardo los estados de los switchers para Global/Local:
        emitters_switchers = defaultdict(list)
        for cp_name in active_emitter.switchers.__annotations__.keys():
            emitters_switchers[id_name].append((cp_name, getattr(active_emitter.switchers, cp_name)))
        
        emitters_list.add_item(id_name, new_emtr_ob.name, group_coll, new_emtr_ob, new_vrtx_ob, new_vrtx_ob_dead, emitters_switchers[id_name])

        # Deselecciona todos los objetos
        bpy.ops.object.select_all(action='DESELECT')

        # Tras duplicar, actualizamos todas las subsecciones no solo la actual, para cargar los settings correspondientes del objeto en las subsections:
        all_subsections = ['PHYSICS', 'SPRINGS', 'EMISSION']
        for subsection in all_subsections:
            ui_update(context, active_emitter, without_self_ob=False, subsection=subsection)

        # Para que se quede seleccionado el ultimo duplicado:
        emitters_list.list_index = emitters_list.list_index

        # Ponemos semillas random a nuestros nuevos emisores de particulas duplicados:
        for psys in new_emtr_ob.particle_systems:
            # Trabajamos solo con nuestras particulas:
            if psys.settings.fluidlab.id_name == "":
                continue
            psys.seed = randint(0, 999999999)


        return {'FINISHED'}


class FLUIDLAB_OT_active_emitter_rm_bake(FLUIDLAB_OT_list_duplication):
    bl_idname = "fluidlab.active_emitter_rm_bake"
    bl_label = "Remove Bake"
    bl_description = "Remove bake from active emitter"
    bl_options = {'REGISTER', 'UNDO'}


    def execute(self, context):

        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active

        if not active_group:
            print("No active group!")
            return {'CANCELLED'}
        
        emitters_list = active_group.emitters
        active_emitter = emitters_list.active

        if not active_emitter:
            print("No active emitter!")
            return {'CANCELLED'}

        emitter_ob = emitters_list.get_current_emitter
        
        if not emitter_ob:
            print("No emitter_ob!")
            return {'CANCELLED'}

        # por todos sus sistemas de particulas:
        for psys in emitter_ob.particle_systems:
            
            # Solo trabajamos con nuestras particulas:
            if psys.settings.fluidlab.id_name == "":
                continue

            with context.temp_override(object=emitter_ob, point_cache=psys.point_cache):
                bpy.ops.ptcache.free_bake()

        return {'FINISHED'}





###########################
""" Dropdowns lists UI  """
###########################


#-------------------------------------------
# Fluid Groups and Emitters Dropdowns menus:
#-------------------------------------------
class FLUIDLAB_MT_group_list_submenu(Menu):
    bl_label = "Group List Submenu"

    def draw(self, context):
        layout = self.layout
        col = layout.column()        
        col.operator("fluidlab.duplicate_group", text="Duplicate Group", icon='DUPLICATE')


class FLUIDLAB_MT_emitters_list_submenu(Menu):
    bl_label = "Emitters List Submenu"

    def draw(self, context):
        layout = self.layout
        col = layout.column()        
        col.operator("fluidlab.duplicate_emitter", text="Duplicate Emitter", icon='DUPLICATE')
        col.operator("fluidlab.active_emitter_rm_bake", text="Remove Bake", icon='TRASH')

#------------------------------------------
# Forces Groups and Forces Dropdowns menus:
#------------------------------------------
class FLUIDLAB_OT_forces_groups_duplicate(FLUIDLAB_OT_list_duplication):
    bl_idname = "fluidlab.forces_groups_duplicate"
    bl_label = "Duplicate Groups"
    bl_description = "Duplicate Emitter Iten"
    bl_options = {'REGISTER', 'UNDO'}

    new_name: StringProperty(default="")

    def get_force_group_name(self, context:Context, with_zfill:bool=False) -> str:

        # Sugiero el nuevo nombre:
        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        ff_active_group = fluid_forces_groups.active
        group_coll = ff_active_group.group_coll
        group_name = group_coll.name

        if with_zfill:
            place = bpy.data.collections
            new_name = name_with_zfill(context, group_name, place)
        else:
            new_name = group_name
        
        return new_name

    def invoke(self, context, event):
        self.new_name = self.get_force_group_name(context, with_zfill=True)
        return context.window_manager.invoke_props_dialog(self, width=250)

    def draw(self, context):
        layout = self.layout
        layout.use_property_decorate = False
        layout.use_property_split = False

        group_name = self.get_force_group_name(context, with_zfill=False)
        new_name = layout.column()
        new_name.alert = self.new_name == group_name or not self.new_name
        new_name.prop(self, "new_name", text="New Name")

    def execute(self, context):
        # DUPLICATE FORCES GROUPS

        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        active_group = fluid_forces_groups.active
        
        if not active_group:
            return {'CANCELLED'}

        main_forces_coll = bpy.data.collections[FluidLabNaming.MAIN_FORCES_COLL]
        set_active_collection(context, main_forces_coll)

        active_group_coll = active_group.group_coll
        current_name = active_group_coll.name
        place = main_forces_coll.children_recursive
        new_name = name_with_zfill(context, current_name, place)

        new_coll = create_new_collection(context, new_name, in_master_coll=False)
        set_active_collection(context, new_coll)

        new_coll[FluidLabNaming.FORCES_GROUP_COLL] = True

        all_new_obs = []
        for ob in active_group_coll.objects:

            # creo el nuevo objeto:
            new_force_ob = ob.copy()

            place = active_group_coll.objects
            new_ob_name = name_with_zfill(context, ob.name, place)
            new_force_ob.name = new_ob_name
            all_new_obs.append(new_force_ob)

            if new_force_ob.name not in new_coll.objects:
                new_coll.objects.link(new_force_ob)
        
        # Lo agregamos al listado:
        id_name = str(uuid4())[:6]
        collection_place = bpy.data.collections
        label_txt = name_with_zfill(context, new_coll.name, collection_place)
        new_item = fluid_forces_groups.add_item(id_name, label_txt, new_coll)

        forces_list = new_item.forces  # el forces list de la nueva coll
        for new_ob in all_new_obs:
            forces_list.add_item(id_name, new_ob.name, new_ob)
        
        return {'FINISHED'}

class FLUIDLAB_OT_forces_groups_forces_duplicate(FLUIDLAB_OT_list_duplication):
    bl_idname = "fluidlab.forces_groups_forces_duplicate"
    bl_label = "Duplicate Forces"
    bl_description = "Duplicate Emitter Iten"
    bl_options = {'REGISTER', 'UNDO'}

    new_name: StringProperty(default="")

    def get_force_ob_name(self, context:Context, with_zfill:bool=False) -> str:

        # Sugiero el nuevo nombre:
        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        ff_active_group = fluid_forces_groups.active
        forces_list = ff_active_group.forces
        active_force = forces_list.active
        force_ob = active_force.force

        if with_zfill:
            place = ff_active_group.group_coll.objects
            new_name = name_with_zfill(context, force_ob.name, place)
        else:
            new_name = force_ob.name

        return new_name

    def invoke(self, context, event):
        self.new_name = self.get_force_ob_name(context, with_zfill=True)
        return context.window_manager.invoke_props_dialog(self, width=250)

    def draw(self, context):
        layout = self.layout
        layout.use_property_decorate = False
        layout.use_property_split = False
        
        force_name = self.get_force_ob_name(context, with_zfill=False)

        new_name = layout.column()
        new_name.alert = self.new_name == force_name or not self.new_name
        new_name.prop(self, "new_name", text="New Name")

    def execute(self, context):
        # DUPLICATE FORCES

        fluid_forces_groups = get_common_vars(context, get_fluid_forces_groups=True)
        active_group = fluid_forces_groups.active
        
        if not active_group:
            return {'CANCELLED'}

        main_forces_coll = bpy.data.collections[FluidLabNaming.MAIN_FORCES_COLL]
        set_active_collection(context, main_forces_coll)

        active_group_coll = active_group.group_coll
        
        forces_list = active_group.forces
        force_active_item = forces_list.active
        force_ob = force_active_item.force

        # Preparo el nombre:
        place = active_group_coll.objects
        new_ob_name = name_with_zfill(context, force_ob.name, place)
        
        all_new_obs = []

        # Creo el nuevo objeto:
        new_force_ob = force_ob.copy()
        new_force_ob.name = new_ob_name
        all_new_obs.append(new_force_ob)

        # los Force Fields tiene un handler y sus hijos las fuerzas, por eso:
        childrens = force_ob.children_recursive
        if len(childrens) > 0:
            for child in childrens:
                if not child.type == 'EMPTY':
                    continue
                new_child_ob = child.copy()
                new_child_ob.name = new_ob_name
                new_child_ob.parent = new_force_ob
                all_new_obs.append(new_child_ob)

        # Lo linkamos donde esté linkeado el original:
        for coll in force_ob.users_collection:
            for new_ob in all_new_obs:
                if new_ob.name not in coll.objects:
                    coll.objects.link(new_ob)
        
        # Lo agregamos al listado:
        id_name = str(uuid4())[:6]
        forces_list.add_item(id_name, new_ob_name, new_force_ob)

        return {'FINISHED'}
    
    
class FLUIDLAB_MT_force_groups_list_submenu(Menu):
    bl_label = "Force Groups List Submenu"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.operator("fluidlab.forces_groups_duplicate", text="Duplicate Group", icon='DUPLICATE')


class FLUIDLAB_MT_force_groups_forces_list_submenu(Menu):
    bl_label = "Forces List Submenu"

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.operator("fluidlab.forces_groups_forces_duplicate", text="Duplicate Forces", icon='DUPLICATE')